Example 2 Clustering Geometric Objects

In this example we will look at a few of the tools provided by the clifford package for (4,1) conformal geometric algebra (CGA) and see how we can use them in a practical setting to cluster geometric objects via the simple K-means clustering algorithm provided in clifford.tools

As before the first step in using the package for CGA is to generate and import the algebra:

[1]:
from clifford.g3c import *
print('e1*e1 ', e1*e1)
print('e2*e2 ', e2*e2)
print('e3*e3 ', e3*e3)
print('e4*e4 ', e4*e4)
print('e5*e5 ', e5*e5)
e1*e1  1
e2*e2  1
e3*e3  1
e4*e4  1
e5*e5  -1

The tools submodule of the clifford package contains a wide array of algorithms and tools that can be useful for manipulating objects in CGA. In this case we will be generating a large number of objects and then segmenting them into clusters.

We first need an algorithm for generating a cluster of objects in space. We will construct this cluster by generating a random object and then repeatedly disturbing this object by some small fixed amount and storing the result:

[2]:
from clifford.tools.g3c import *
import numpy as np

def generate_random_object_cluster(n_objects, object_generator, max_cluster_trans=1.0, max_cluster_rot=np.pi/8):
    """ Creates a cluster of random objects """
    ref_obj = object_generator()
    cluster_objects = []
    for i in range(n_objects):
        r = random_rotation_translation_rotor(maximum_translation=max_cluster_trans, maximum_angle=max_cluster_rot)
        new_obj = apply_rotor(ref_obj, r)
        cluster_objects.append(new_obj)
    return cluster_objects
/home/docs/checkouts/readthedocs.org/user_builds/clifford/conda/v1.2.0/lib/python3.7/site-packages/pyganja/__init__.py:2: UserWarning: Failed to import cef_gui, cef functions will be unavailable
  from .script_api import *

We can use this function to create a cluster and then we can visualise this cluster with pyganja.

[3]:
from pyganja import *
clustered_circles = generate_random_object_cluster(10, random_circle)
sc = GanjaScene()
for c in clustered_circles:
    sc.add_object(c, rgb2hex([255,0,0]))
draw(sc, scale=0.05)

This cluster generation function appears in clifford tools by default and it can be imported as follows:

[4]:
from clifford.tools.g3c import generate_random_object_cluster

Now that we can generate individual clusters we would like to generate many:

[5]:
def generate_n_clusters( object_generator, n_clusters, n_objects_per_cluster ):
    object_clusters = []
    for i in range(n_clusters):
        cluster_objects = generate_random_object_cluster(n_objects_per_cluster, object_generator,
                                                         max_cluster_trans=0.5, max_cluster_rot=np.pi / 16)
        object_clusters.append(cluster_objects)
    all_objects = [item for sublist in object_clusters for item in sublist]
    return all_objects, object_clusters

Again this function appears by default in clifford tools and we can easily visualise the result:

[6]:
from clifford.tools.g3c import generate_n_clusters

all_objects, object_clusters = generate_n_clusters(random_circle, 2, 5)
sc = GanjaScene()
for c in all_objects:
    sc.add_object(c, rgb2hex([255,0,0]))
draw(sc, scale=0.05)

Given that we can now generate multiple clusters of objects we can test algorithms for segmenting them.

The function run_n_clusters below generates a lot of objects distributed into n clusters and then attempts to segment the objects to recover the clusters.

[7]:
from clifford.tools.g3c.object_clustering import n_clusters_objects
import time

def run_n_clusters( object_generator, n_clusters, n_objects_per_cluster, n_shotgunning):
    all_objects, object_clusters = generate_n_clusters( object_generator, n_clusters, n_objects_per_cluster )
    [new_labels, centroids, start_labels, start_centroids] = n_clusters_objects(n_clusters, all_objects,
                                                                                initial_centroids=None,
                                                                                n_shotgunning=n_shotgunning,
                                                                                averaging_method='unweighted')
    return all_objects, new_labels, centroids

Lets try it!

[8]:
def visualise_n_clusters(all_objects, centroids, labels,
                         color_1=np.array([255, 0, 0]),
                         color_2=np.array([0, 255, 0])):
    """
    Utility method for visualising several clusters and their respective centroids
    using pyganja
    """
    alpha_list = np.linspace(0, 1, num=len(centroids))
    sc = GanjaScene()
    for ind, this_obj in enumerate(all_objects):
        alpha = alpha_list[labels[ind]]
        cluster_color = (alpha * color_1 + (1 - alpha) * color_2)
        sc.add_object(this_obj, rgb2hex(cluster_color))

    for c in centroids:
        sc.add_object(c, Color.BLACK)

    return sc



object_generator = random_circle

n_clusters = 3
n_objects_per_cluster = 10
n_shotgunning = 60
all_objects, labels, centroids = run_n_clusters(object_generator, n_clusters,
                                                     n_objects_per_cluster, n_shotgunning)

sc = visualise_n_clusters(all_objects, centroids, labels,
                          color_1=np.array([255, 0, 0]),
                          color_2=np.array([0, 255, 0]))
draw(sc, scale=0.05)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-a9efef63933e> in <module>
     30 sc = visualise_n_clusters(all_objects, centroids, labels,
     31                           color_1=np.array([255, 0, 0]),
---> 32                           color_2=np.array([0, 255, 0]))
     33 draw(sc, scale=0.05)

<ipython-input-8-a9efef63933e> in visualise_n_clusters(all_objects, centroids, labels, color_1, color_2)
     11         alpha = alpha_list[labels[ind]]
     12         cluster_color = (alpha * color_1 + (1 - alpha) * color_2)
---> 13         sc.add_object(this_obj, rgb2hex(cluster_color))
     14
     15     for c in centroids:

~/checkouts/readthedocs.org/user_builds/clifford/conda/v1.2.0/lib/python3.7/site-packages/pyganja/color.py in rgb2hex(x)
      7         val = 0
      8         for xi in x:
----> 9             val = (val << 8) | _entry_as_uint8(xi)
     10         return val
     11     else:

~/checkouts/readthedocs.org/user_builds/clifford/conda/v1.2.0/lib/python3.7/site-packages/pyganja/color.py in _entry_as_uint8(v)
     22             return operator.index(v)
     23     else:
---> 24         raise TypeError("Can't convert {!r} to a color component".format(v))
     25
     26

TypeError: Can't convert 127.5 to a color component